package distributelock

import (
	"context"
	"sync"
	"time"

	"github.com/go-redis/redis"
)

/*
红锁：
	1. 向多个Redis实例发送SETNX命令，尝试获取锁
	2. 当超过一半的实例成功获取锁时，认为获取成功
	3. 如果获取锁成功，需要设置一个定时器，定期对锁进行续期

推荐拓展库：redsync
次文件不使用redsync库，而是自己实现,搭配看门狗机制，实现分布式锁
*/

type RedLock struct {
	clients     []*redis.Client // 多个redis实例
	succClients []*redis.Client // 成功获取锁的redis实例
	resource    string          // 锁的资源名称
	randomValue string          // 随机值
	watchDog    chan struct{}   // 看门狗
	ttl         time.Duration   // 锁的过期时间
}

func NewRedLock(clients []*redis.Client, resource, randomValue string, ttl time.Duration) *RedLock {
	return &RedLock{
		clients:     clients,
		succClients: make([]*redis.Client, 0),
		resource:    resource,
		randomValue: randomValue,
		watchDog:    make(chan struct{}),
		ttl:         ttl,
	}
}

func (r *RedLock) TryLock(ctx context.Context) error {
	var wg sync.WaitGroup
	var mu sync.Mutex
	wg.Add(len(r.clients))
	//成功获取锁的实例
	succClients := make([]*redis.Client, 0)
	for _, client := range r.clients {
		go func(client *redis.Client) {
			defer wg.Done()
			succ, err := client.SetNX(r.resource, r.randomValue, r.ttl).Result()
			if err != nil {
				return
			}

			if !succ {
				return
			}

			mu.Lock()
			succClients = append(succClients, client)
			mu.Unlock()
		}(client)
	}

	//等待所有获取锁操作完成
	wg.Wait()

	// 如果成功获取锁的实例数量少于一半，则认为获取锁失败
	if len(succClients) < len(r.clients)/2+1 {
		//把已经成功获取锁的实例释放掉
		for _, client := range succClients {
			go func(client *redis.Client) {
				_, cancel := context.WithTimeout(context.Background(), ttl)
				defer cancel()
				client.Eval(unlockScript, []string{r.resource}, r.randomValue)

			}(client)
		}

		return ErrLockFailed
	}

	// 启动看门狗
	go r.startWatchDog()
	// 记录成功获取锁的实例
	r.succClients = succClients
	return nil
}

func (r *RedLock) startWatchDog() {
	ticker := time.NewTicker(resetTTLInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C: //延长锁的过期时间
			for _, client := range r.succClients {
				go func(client *redis.Client) {
					_, cancel := context.WithTimeout(context.Background(), ttl)
					defer cancel()
					client.Expire(r.resource, ttl)
				}(client)
			}
		case <-r.watchDog:
			return
		}
	}
}

// 释放锁
func (r *RedLock) Unlock() error {
	// 停止看门狗
	close(r.watchDog)
	// 释放锁
	var wg sync.WaitGroup
	wg.Add(len(r.succClients))
	for _, client := range r.succClients {
		go func(client *redis.Client) {
			defer wg.Done()
			_, cancel := context.WithTimeout(context.Background(), ttl)
			defer cancel()
			client.Eval(unlockScript, []string{r.resource}, r.randomValue)
		}(client)
	}

	wg.Wait()
	return nil
}
